package engine.gameObject.model;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.util.vector.Vector3f;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;
import engine.gameObject.GameObject;
import engine.gameObject.RenderObject;
import engine.gameObject.collider.BoundingSphere;
import engine.gameObject.collider.Collider;
import engine.gameObject.collider.SphereCollider;
import engine.material.Material;
import engine.shader.Shader;

public class Model extends RenderObject {
	
	private boolean hasTexCoords;
	
	private int texture;
	
	private int vaoID;
	private int vboID;
	
	private int indexPointer;
	
	private int triangleCount;
	
	/**
	 * Creates a new model with vertices and normals. If you dont want to use texture coordinates, then set texCoords to null. 
	 * @param vertices
	 * @param normals
	 * @param texCoords
	 */
	public Model(IntBuffer indexbuffer, FloatBuffer verticeBuffer, FloatBuffer normalBuffer, FloatBuffer texCoordBuffer, Material material, int texture, int collisionLevels) {
		if(verticeBuffer == null || normalBuffer == null) {
			throw new NullPointerException("Model creation requires vertices and normals");
		}
		
		this.texture = texture;
		this.hasTexCoords = texCoordBuffer != null;
		this.triangleCount = indexbuffer.capacity();
		
		setMaterial(material);
		
		createVAO(indexbuffer, verticeBuffer, normalBuffer, texCoordBuffer);
		
		computeBoundingRadius(verticeBuffer);
		computeCollisionTree(verticeBuffer, indexbuffer, collisionLevels);
	}

	public Model(int indexPointer, int vaoID, Material material, int texture, boolean hasTexCoords, int triangleCount) {
		this.texture = texture;
		this.vaoID = vaoID;
		
		this.hasTexCoords = hasTexCoords;
		this.triangleCount = triangleCount;
		
		this.indexPointer = indexPointer;
		
		setMaterial(material);
	}
	
	/**
	 * Creates the vbos out of the given data. If texCoords are null, be sure that hasTexCoords is set to false. 
	 * @param vertices
	 * @param normals
	 * @param texCoords
	 */
	private void createVAO(IntBuffer indexbuffer, FloatBuffer verticeBuffer, FloatBuffer normalBuffer, FloatBuffer texCoordBuffer) {
		//create indices
		indexPointer = glGenBuffers();
		indexbuffer.rewind();
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexPointer);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexbuffer, GL_STATIC_DRAW);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
		
		FloatBuffer totalBuffer = BufferUtils.createFloatBuffer(verticeBuffer.capacity() + normalBuffer.capacity() + (texCoordBuffer != null ? texCoordBuffer.capacity() : 0));
		verticeBuffer.rewind();
		totalBuffer.put(verticeBuffer);
		if(texCoordBuffer != null) {
			texCoordBuffer.rewind();
			totalBuffer.put(texCoordBuffer);
		}
		normalBuffer.rewind();
		totalBuffer.put(normalBuffer);
		totalBuffer.rewind();
		
		vboID = glGenBuffers();
		glBindBuffer(GL_ARRAY_BUFFER, vboID);
		glBufferData(GL_ARRAY_BUFFER, totalBuffer, GL_STATIC_DRAW);
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		
		vaoID = glGenVertexArrays();
		glBindVertexArray(vaoID);
		
		glBindBuffer(GL_ARRAY_BUFFER, vboID);
		
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 3, GL_FLOAT, false, 0, (verticeBuffer.capacity() + (texCoordBuffer != null ? texCoordBuffer.capacity() : 0)) * 4);
		
		if(texCoordBuffer != null) {
			glEnableVertexAttribArray(1);
			glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, verticeBuffer.capacity() * 4);
		}
		
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		glBindVertexArray(0);
	}
	
	private void computeBoundingRadius(FloatBuffer verticeBuffer) {
		float currentRadius = 0;
		
		verticeBuffer.rewind();
		for(int i = 0; i < verticeBuffer.capacity(); i += 3) {
			float curDistance = verticeBuffer.get(i) * verticeBuffer.get(i) + verticeBuffer.get(i + 1) * verticeBuffer.get(i + 1) + verticeBuffer.get(i + 2) * verticeBuffer.get(i + 2);
			
			currentRadius = Math.max(currentRadius, curDistance);
		}
		
		setBoundingRadius((float)Math.sqrt(currentRadius));
	}
	
	private void computeCollisionTree(FloatBuffer verticeBuffer, IntBuffer indices, int levels) {
		//compute bounding box
		float[] min = new float[]{Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE};
		float[] max = new float[]{Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE};
		
		verticeBuffer.rewind();
		for(int i = 0; i < verticeBuffer.capacity(); i += 3) {
			min[0] = Math.min(verticeBuffer.get(i + 0), min[0]);
			min[1] = Math.min(verticeBuffer.get(i + 1), min[1]);
			min[2] = Math.min(verticeBuffer.get(i + 2), min[2]);
			
			max[0] = Math.max(verticeBuffer.get(i + 0), max[0]);
			max[1] = Math.max(verticeBuffer.get(i + 1), max[1]);
			max[2] = Math.max(verticeBuffer.get(i + 2), max[2]);
		}
		
		//enlarge to a cube (depending on the largest size)
		float sideLength = Math.max(Math.max(max[0] - min[0], max[1] - min[1]), max[2] - min[2]);
		float radius = (float)Math.sqrt(3 * (sideLength / 2) * (sideLength / 2));
		float[] center = new float[3];
		for(int i = 0; i < 3; i++) {
			center[i] = (max[i] - min[i]) / 2 + min[i];
			min[i] = center[i] - sideLength / 2;
			max[i] = center[i] + sideLength / 2;
		}
		
		BoundingSphere root = new BoundingSphere(radius, center);
		//compute the childen
		computeChildren(root, sideLength / 2, levels, verticeBuffer, indices);
		
		//add collider to the model
		if(root.getChildren().size() > 0) {
			SphereCollider sphereCollider = new SphereCollider();
			
			for(BoundingSphere sphere: root.getChildren()) {
				sphereCollider.addSphere(sphere);
			}
			
			setCollider(sphereCollider);
		}
	}
	
	private void computeChildren(BoundingSphere sphere, float sideLength, int level, FloatBuffer verticeBuffer, IntBuffer indices) {
		if(level <= 0) {
			return;
		}
		
		float radius = (float)Math.sqrt(3 * (sideLength / 2) * (sideLength / 2));
		for(int i = 0; i < 8; i++) {
			Vector3f center = new Vector3f();
			center.x = sphere.getPosition()[0] + ((i % 2 == 1) ? sideLength / 2 : -sideLength / 2);
			center.y = sphere.getPosition()[1] + (((i >> 1) % 2 == 1) ? sideLength / 2 : -sideLength / 2);
			center.z = sphere.getPosition()[2] + (((i >> 2) % 2 == 1) ? sideLength / 2 : -sideLength / 2);
			
			if(boxContainsPolygon(center, sideLength / 2, verticeBuffer, indices)) {
				BoundingSphere newSphere = new BoundingSphere(radius, new float[]{center.x, center.y, center.z});
				computeChildren(newSphere, sideLength / 2, level - 1, verticeBuffer, indices);
				sphere.addSphere(newSphere);
			}
		}
	}
	
	private boolean boxContainsPolygon(Vector3f center, float sideLength, FloatBuffer verticeBuffer, IntBuffer indices) {
		for(int i = 0; i < indices.capacity(); i += 3) {
			if(Collider.polygonInBox(center, sideLength, new Vector3f(verticeBuffer.get(indices.get(i) * 3), 
														verticeBuffer.get(indices.get(i) * 3 + 1), 
														verticeBuffer.get(indices.get(i) * 3 + 2)) ,
														new Vector3f(
														verticeBuffer.get(indices.get(i + 1) * 3), 
														verticeBuffer.get(indices.get(i + 1) * 3 + 1), 
														verticeBuffer.get(indices.get(i + 1) * 3 + 2)),
														new Vector3f(
														verticeBuffer.get(indices.get(i + 2) * 3), 
														verticeBuffer.get(indices.get(i + 2) * 3 + 1), 
														verticeBuffer.get(indices.get(i + 2) * 3 + 2)))) {
				return true;
			}
			
		}
		return false;

	}
	
	@Override
	public void renderImpl(Shader shader) {
		
		getScene().addDrawCall();
		getScene().addPolygons(triangleCount / 3);
		
		if(texture != 0) {
			glBindTexture(GL_TEXTURE_2D, texture);
			shader.sendUniform1i("useTexture", 1);
		} else {
			shader.sendUniform1i("useTexture", 0);
		}
		
		glBindVertexArray(vaoID);
	    
	    //paint according to indices
	    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexPointer);
	    glDrawElements(GL_TRIANGLES, triangleCount, GL_UNSIGNED_INT, 0);
		
		// clean up
	    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	    glBindVertexArray(0);
	}

	public int getTexture() {
		return texture;
	}

	public void setTexture(int texture) {
		this.texture = texture;
	}
	
	@Override
	public GameObject copy() {
		Model model = new Model(indexPointer, vaoID, getMaterial().copy(), texture, hasTexCoords, triangleCount);
		
		model.setId(getId());
		
		model.setPosition(getPosition());
		model.setScale(getScale());
		
		model.setShader(getShader());
		
		model.setBoundingRadius(getBoundingRadius());
		model.setCollider(getCollider().copy());
		model.setRigidbody(isRigidbody());
		model.setColliding(isColliding());
		
		for(GameObject g: getChildren()) {
			model.addChild(g.copy());
		}
		
		return model;
	}
}
